Skip to content

[HDX-5079] Render formulas in the composed metric query - #2908

Merged
kodiakhq[bot] merged 5 commits into
mainfrom
warren/HDX-5079-render-formulas
Aug 17, 2026
Merged

[HDX-5079] Render formulas in the composed metric query#2908
kodiakhq[bot] merged 5 commits into
mainfrom
warren/HDX-5079-render-formulas

Conversation

@wrn14897

Copy link
Copy Markdown
Member

Summary

Renders metric formulas (HDX-5078's formulas config) in the composed multi-series metric query (HDX-5077), so a derived series like A / (A + B + C) * 100 is computed by ClickHouse as part of the single composed query instead of not rendering at all.

  • compileFormulaAst (core/formula.ts): compiles the validated letter-ref AST into a SQL expression over per-series value expressions. Never splices user text into SQL — only the parsed/validated AST is walked.
  • Missing-data semantics match the existing ratio projection: a series ref compiles to coalesce(<pivot>, 0) (a missing operand counts as 0, so a zero-error group reads 0%, not N/A), and every division denominator is wrapped in nullif(..., 0) (zero or missing denominator → NULL → rendered gap, never 0 or an error).
  • Projection order (renderMultiSeriesMetricChartConfig): operand value columns first in select order, then formula columns in formulas order, ahead of the group/bucket passthrough columns — preserving the useChartNumberFormats positional meta contract. showOperandSeries: false drops the operand columns so only the formula column(s) are returned.
  • Routing: a single-series metric chart with a formula (e.g. A * 100) now routes through the composed path (single-branch union pivot). Per-series branches strip formulas to avoid recursion.
  • Precedence: formulas supersede seriesReturnType: 'ratio' (the two are mutually exclusive in the editor; the renderer stays deterministic on a hand-built config carrying both).
  • builderToRawSql: formula configs are rejected from "convert to SQL" with a clear message (same limitation as multi-series metric charts).
  • Formulas are validated at render time with the structured validator; an invalid persisted expression throws a descriptive error rather than a ClickHouse error.

Alerts on formula tiles work with no changes since they query through queryChartConfigrenderChartConfig.

Testing

  • make ci-lint, make ci-unit pass.
  • New unit tests: compileFormulaAst (precedence, nested divisions, unary minus, HDX-4938 example) and SQL snapshot tests for the formula projection (grouped, hidden operands, single-series routing, formula-vs-ratio precedence, alias collision suffixing, invalid-formula error).
  • New integration tests in queryChartConfig.int.test.ts (all passing against the docker ClickHouse, with the HDX-5076/5077 regression baseline unchanged):
    • HDX-4938 motivating example A / (A + B + C) * 100
    • Division by zero / missing denominator → gap, not 0 or error (pinned against the same fixture as the seriesReturnType: 'ratio' test for drop-in parity)
    • Formula over mixed gauge + sum (increase) operands across tables
    • Grouped formula computed per (bucket, group) row
    • Meta contract with operand series shown vs hidden
    • Number- and table-shape formulas

How to test on Vercel preview

N/A — query-rendering change in common-utils; the chart editor UI for formulas lands in HDX-5080.

References

@wrn14897 wrn14897 added the ai-generated AI-generated content; review carefully before merging. label Aug 13, 2026
@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d06dbba

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 17, 2026 10:37pm
hyperdx-storybook Ready Ready Preview Aug 17, 2026 10:37pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds SQL compilation and composed-query rendering for metric formulas, including single-series routing and raw-SQL template conversion.

  • Compiles validated formula ASTs into ClickHouse expressions with explicit missing-value and zero-denominator semantics.
  • Projects formula columns alongside or instead of operand columns while preserving positional metadata ordering.
  • Escapes composed-query output aliases and adds unit, snapshot, and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported malformed alias quoting is fixed across all composed projection paths.

Important Files Changed

Filename Overview
packages/common-utils/src/core/formula.ts Adds a validated-AST-to-SQL compiler with coalesced operands and guarded division.
packages/common-utils/src/core/renderChartConfig.ts Adds formula-aware composed projections and correctly fixes quoted aliases across formula, operand, and ratio columns.
packages/common-utils/src/core/builderToRawSql.ts Enables composed multi-series, ratio, and formula metric queries in generated raw-SQL templates.
packages/common-utils/src/tests/queryChartConfig.int.test.ts Covers formula execution, missing-data behavior, mixed metric types, grouping, metadata ordering, and supported display shapes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Config[Metric chart config] --> Validate[Validate formula AST]
  Validate --> Branches[Render one query branch per operand]
  Branches --> Union[UNION ALL]
  Union --> Pivot[Pivot values by series index]
  Pivot --> Compile[Compile formula over pivot expressions]
  Compile --> Project[Project operands and formula aliases]
  Project --> ClickHouse[Execute composed ClickHouse query]
Loading

Reviews (6): Last reviewed commit: "Merge branch 'main' into warren/HDX-5079..." | Re-trigger Greptile

Comment thread packages/common-utils/src/core/renderChartConfig.ts Outdated
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 299 passed • 1 skipped • 1081s

Status Count
✅ Passed 299
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@wrn14897
wrn14897 marked this pull request as ready for review August 14, 2026 04:22
@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Additional context: touches the query rendering engine lightly (142 lines, under the 150-line bar for Tier 4)

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 3
  • Production lines changed: 188 (+ 981 in test files, excluded from tier calculation)
  • Branch: warren/HDX-5079-render-formulas
  • Author: wrn14897

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Rendering metric formulas (A / (A + B + C) * 100) into the composed multi-series ClickHouse metric query. I read every changed line in formula.ts, renderChartConfig.ts, and builderToRawSql.ts, plus the supporting parser/validator and config schema. The core is sound: compileFormulaAst walks only the validated AST (never splicing raw expression text), numeric literals are digits-only, validateFormula runs before compilation, and quotedColumnName now escapes embedded double quotes at every emission site inside renderMultiSeriesMetricChartConfig (formula, operand, ratio, and plain paths). Single-series charts without a formula are unaffected by the new routing, and the value-columns-first meta ordering that useChartNumberFormats depends on is preserved.

✅ No critical issues found. No P0/P1 defects; the diff is safe to merge. One test gap and a couple of small maintainability nits below.

🟡 P2 — recommended

  • packages/common-utils/src/core/builderToRawSql.ts:100 — The new branch that rejects formula-bearing metric configs from raw-SQL conversion (including single-series configs with formulas, which previously converted) and its distinct error message are untested; a refactor flipping the ternary or dropping the formulas clause passes CI silently.
    • Fix: Add a builderToRawSql test asserting a single-series metric config carrying formulas returns isError: true with the formula-specific message.
    • testing
🔵 P3 nitpicks (2)
  • packages/common-utils/src/core/renderChartConfig.ts:2473 — The per-series value-column projection (valueExprFor(splitIdx) AS quotedColumnName(...)) is emitted byte-identically in both the hasFormulas operand branch and the plain else branch, so a future change to value-column emission must be made twice or the two silently diverge.
    • Fix: Extract a shared closure that both branches call to push the ordered value columns.
    • maintainability
  • packages/common-utils/src/core/builderToRawSql.ts:102 — The formula-presence check (config.formulas?.length ?? 0) > 0 is re-implemented inline (twice) rather than reusing the exported hasMetricFormulas helper, duplicating the definition of "what counts as a formula chart" across modules.
    • Fix: Import and call hasMetricFormulas in place of the inline expression.
    • maintainability

Reviewers (8 dispatched): correctness, security, adversarial, testing, maintainability, kieran-typescript, api-contract, learnings-researcher. Graded findings reflect the testing and maintainability reviewers plus direct orchestrator analysis of the correctness, security, and API-contract dimensions (all clean); the learnings researcher found no prior docs/solutions/ guidance on ClickHouse SQL generation or the composed-metric query.

Testing gaps:

  • The number-chart (no passthrough columns) formula projection with implicit global aggregation is exercised only by the live-ClickHouse integration test, not by a fast unit-level SQL-shape assertion in renderChartConfig.test.ts.
  • Pre-existing / out of scope: the general renderSelect path (renderChartConfig.ts:817) still interpolates a user alias into an AS "..." identifier without escaping — the natural home for the new quotedColumnName helper. Not introduced by this diff; worth a follow-up.

pulpdrew
pulpdrew previously approved these changes Aug 17, 2026

@pulpdrew pulpdrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with a comment that could be a followup if it makes sense.

Comment on lines +99 to +109
if (
isMetric &&
Array.isArray(config.select) &&
(config.select.length > 1 || (config.formulas?.length ?? 0) > 0)
) {
return {
isError: true,
error: 'Multi-series metric charts cannot be auto-converted to SQL.',
error:
(config.formulas?.length ?? 0) > 0
? 'Metric charts with formulas cannot be auto-converted to SQL.'
: 'Multi-series metric charts cannot be auto-converted to SQL.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a limit originally because multi-series metrics couldn't be represented as a single query. Now they can, and so can formulas + ratios. Is there a reason to continue blocking this case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I believe we should be able to show the query now. Let me revisit the code here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No technical reason anymore — you're right that the composed shape lifted the original constraint. I checked what was left:

  • The per-series branches inherit isRenderingRawSqlTemplate, so each branch already renders with $__sourceTable(<metricType>) and the time/interval macros, same as the single-series conversion.
  • replaceMacros resolves multiple $__sourceTable(type) occurrences with per-occurrence args, $__filters lands once per branch source CTE (never in the outer pivot), the hoisted SETTINGS are literal, and the outer pivot/formula projection binds no params.

The only reason it was still blocked was scope — HDX-5077 listed builderToRawSql multi-series support as a follow-up, and this gate just extended that to formulas with a clearer message.

Lifted in d63af10: multi-series, ratio, and formula metric charts now convert to a macro-based template (with snapshot tests, $__filters-placement coverage, and a replaceMacros round-trip). Non-time-series metric charts keep the existing restriction.

Compile the validated formula AST (HDX-5078) into the final SELECT
projection over the pivoted per-series columns produced by the composed
multi-series metric query (HDX-5077).

- compileFormulaAst: letter refs resolve to the pivot expressions with
  ratio-consistent semantics (missing operand -> 0, division by zero or
  missing denominator -> NULL, rendered as a gap)
- Formula columns append after the operand value columns (select order),
  preserving the useChartNumberFormats positional meta contract;
  showOperandSeries: false emits only the formula column(s)
- Single-series metric charts with a formula route through the composed
  path; formulas take precedence over seriesReturnType: 'ratio'
- builderToRawSql rejects formula configs (same limitation as
  multi-series metric charts)
A formula or series alias containing a double quote terminated the
double-quoted identifier early (AS "bad"name") and failed the query
with a ClickHouse syntax error. Escape by doubling the quote at
SQL-emission time across all composed-projection sites (formula,
operand, and ratio columns); collision dedup and result meta keep the
raw name.

Addresses greptile P1 review comment.
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This is a self-contained SQL-rendering change with heavy unit + integration test coverage. The two highest-risk surfaces were verified clean:

  • SQL injection: compileFormulaAst walks only the validated AST (parseFormula/validateFormula) and never splices raw expression text into SQL. All four composed-path identifier emission sites (operand, formula, ratio, else) route through the new quotedColumnName, which correctly doubles " for ClickHouse double-quoted identifiers. The formula column's raw-expression fallback name is escaped at every emission site.
  • Missing-data semantics: a series ref compiles to coalesce(<pivot>, 0) and every division denominator is wrapped in nullif(..., 0), matching the existing seriesReturnType: 'ratio' projection so a missing operand reads 0 and a zero/missing denominator renders a gap.
  • Routing & recursion: single-series-with-formula correctly diverts to the composed path; per-series branch configs strip formulas/showOperandSeries, preventing re-entry. Formula/ratio precedence is deterministic. Formula columns contain aggregates, so GROUP BY ALL excludes them exactly as the ratio path relies on.

🟡 P2 -- recommended

  • packages/common-utils/src/core/renderChartConfig.ts:818 -- the single-series alias emission (AS "${{ UNSAFE_RAW_SQL: select.alias }}") does not escape embedded double quotes, unlike the composed path this diff hardened; a select.alias containing " breaks the identifier. This is pre-existing and untouched by the diff, but now stands out as the one remaining unescaped alias site.
    • Fix: route this alias through the same quotedColumnName helper so single-series and composed paths escape identifiers consistently.
🔵 P3 nitpicks (2)
  • packages/common-utils/src/core/formula.ts (compileFormulaAst, number case) -- the comment claims JS stringification yields "plain digits / decimal notation," but a large or tiny literal stringifies to exponential form (e.g. 1e+21, 1e-7); these are still valid ClickHouse float literals so no query error results, but the comment overstates the guarantee.

    • Fix: reword the comment to acknowledge exponential notation for extreme magnitudes, or note that ClickHouse accepts scientific-notation float literals.
  • packages/common-utils/src/__tests__/formula.test.ts -- no compileFormulaAst case exercises a numeric literal that stringifies to exponential notation, leaving that (benign) emission branch unasserted.

    • Fix: add a compileFormulaAst assertion for a large/small numeric literal to pin the emitted SQL text.

Reviewers: synthesized from direct diff analysis across correctness, security/SQL-injection, testing, and maintainability lenses. (The dispatched persona sub-agents had not returned findings at synthesis time; findings above are verified against the diff and surrounding code.)

Testing gaps: formula numeric literals that render in exponential notation are not covered by an explicit assertion, though the resulting SQL is valid.

The multi-series gate in renderBuilderConfigAsSqlTemplate predated the
composed single-query renderer: each per-series branch now emits its own
$__sourceTable(<metricType>) and time macros, $__filters lands once per
branch source CTE, the hoisted SETTINGS are literal, and the outer pivot
binds no params — so multi-series, ratio, and formula metric charts
convert to a raw-SQL template like any other metric chart. Non-time-series
metric charts keep the existing restriction.

Addresses review feedback on the formula gate.
@kodiakhq
kodiakhq Bot merged commit 3ecf73c into main Aug 17, 2026
40 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated AI-generated content; review carefully before merging. automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants